package com.owg.jigsaw.javafx;

import com.owg.jigsaw.WBlock;
import com.owg.jigsaw.slots.Slot;

import owg.util.data.PField;
import owg.util.data.ReferenceSet;
import owg.util.data.Referencer;
import javafx.beans.property.ObjectProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;

public class FXSlot extends Pane
{	
	class LayoutInfo
	{
		protected boolean isLayouting = false;
		
		protected int x;
		protected int y;
		
		protected Integer assignedW;
		protected Integer assignedH;
		protected int prefW;
		protected int prefH;
		
		
		public LayoutInfo()
		{
			x = (int) layoutXProperty().get();
			y = (int) layoutYProperty().get();
			recompute(null, null);
		}
		
		protected void recompute(Integer assignedW, Integer assignedH)
		{
			if(isLayouting)
				new Throwable("Warning: Attempt to recompute while layout is already in progress: ").printStackTrace();
			else
			{
				isLayouting = true;
	
				this.assignedW = assignedW;
				this.assignedH = assignedH;
				
				if(isListConnectorSlot)
				{
					if(model.isHorizontal())
					{
						prefW = FXBlock.TAB_OFFSET;
						prefH = getSlotHeight()+FXBlock.TAB_OFFSET;
					}
					else
					{
						prefW = getSlotWidth()+FXBlock.TAB_OFFSET;
						prefH = FXBlock.TAB_OFFSET;
					}
				}
				else
				{
					int[] straightDim = new int[blocks.length];
					
					prefW = 0;
					prefH = 0;
					
					int sumWidth = 0;
					int sumHeight = 0;
					int maxWidth = FXBlock.TAB_OFFSET+getSlotWidth();
					int maxHeight = FXBlock.TAB_OFFSET+getSlotHeight();
					
					for(int i = 0; i<blocks.length; i++)
					{
						int w = (int) blocks[i].layout.prefW;
						int h = (int) blocks[i].layout.prefH;
						if(model.isHorizontal())
							straightDim[i] = w;
						else
							straightDim[i] = h;
						sumWidth += w-1;
						sumHeight += h-1;
						maxWidth = Math.max(maxWidth, w);
						maxHeight = Math.max(maxHeight, h);
					}
					if(model.isHorizontal())
					{
						prefW = getSlotWidth()-1;
						prefW += sumWidth;
						if(sumWidth == 0 && model.isMultiSlot)
							prefW += FXBlock.OPEN_C_BLOCK_SPACING;
						prefH = maxHeight;
					}
					else
					{
						prefH = getSlotHeight()-1;
						prefH += sumHeight;
						if(sumHeight == 0 && model.isMultiSlot)
							prefH += FXBlock.OPEN_C_BLOCK_SPACING;
						prefW = maxWidth;
					}
					if(blocks.length > 0)
					{
						//Free width is the assigned width minus the preferred width, and this will be allocated to blocks.
						int freeDim, freeIncr;
						if(model.isHorizontal())
						{
							freeDim = getWidth()-prefW;
							freeIncr = freeDim/blocks.length;
						}
						else
						{
							freeDim = getHeight()-prefH;
							freeIncr = freeDim/blocks.length;
						}
						for(int i = 0; i<blocks.length; i++)
						{
							straightDim[i] += freeIncr;
							freeDim -= freeIncr;
						}
						//Rounding error goes to last one
						straightDim[straightDim.length-1] += freeDim;
						
						int offset;
						if(model.isHorizontal())
							offset = getSlotWidth()-1;
						else
							offset = getSlotHeight()-1;
						for(int i = 0; i<blocks.length; i++)
						{
							if(model.isHorizontal())
							{
								blocks[i].setBlockLocation(offset, 0);
								blocks[i].setBlockSize(straightDim[i], getBlocksHeight());
								offset+=blocks[i].getRealWidth()-1;
							}
							else
							{
								blocks[i].setBlockLocation(0, offset);
								blocks[i].setBlockSize(getBlocksWidth(), straightDim[i]);
								offset+=blocks[i].getRealHeight()-1;
							}
						}
					}
				}
				isLayouting = false;
	
				//The assigned size will be non-null iff the layout call comes from the parent in the layout tree.
				//So, if it was null, notify the parent so it can update the size.
				if(assignedW == null)
					layoutListeners.fireEvents();
				requestLayout();
			}
		}

		/**Returns the assigned width, or the preferred width if none has been assigned.*/
		public int getWidth()
		{
			if(assignedW == null)
				return prefW;
			return assignedW;
		}
		/**Returns the assigned height, or the preferred height if none has been assigned.*/
		public int getHeight()
		{
			if(assignedH == null)
				return prefH;
			return assignedH;
		}

		/**Returns the width of the space reserved for blocks*/
		public int getBlocksWidth()
		{
			if(model.isHorizontal())
				return getWidth()-getSlotWidth();
			else
				return getWidth();
		}
		/**Returns the height of the space reserved for blocks*/
		public int getBlocksHeight()
		{
			if(model.isHorizontal())
				return getHeight();	
			else
				return getHeight()-getSlotHeight();
		}
	}
	final ActionSource layoutListeners = new ActionSource();
	
	protected final Referencer referencer = new ReferenceSet();
	protected final EventHandler<ActionEvent> layoutListener;
	protected LayoutInfo layout;
	protected boolean authorizeLocChange = false;
	/**The color property, which is associated with the FXBlock that the slot is added to.
	 * Note that in the case of list connector slot, this is different from the block of the model.*/
	protected final ObjectProperty<Color> colorProp;
	/**Whether this slot exists only for the purpose of connecting one block to a sibling block in a list.
	 * These slots are just fluff.*/
	protected final boolean isListConnectorSlot;
	protected final Slot model;
	/**The image of the slot.*/
	protected FXColoredImageView slot;
	/**A tab image that appears at the bottom of slot-lists only. Note that list connector slots never have this feature.*/
	protected FXColoredImageView tab;
	
	protected FXBlock[] blocks;
	
	
	public FXSlot(Slot model, ObjectProperty<Color> color, boolean isListConnectorSlot)
	{
		super();
		this.colorProp = color;
		this.model = model;
		this.isListConnectorSlot = isListConnectorSlot;
		
		rebuildSlotAndTab();
		//Rebuild slot and tab if the slot's type changes(note: weak listener)
		model.slotType.addListener(referencer, (prop, oldVal, newVal) -> {rebuildSlotAndTab();});
		model.getRoot().getTypeHierarchy().connectorWidth.addListener(referencer, (prop, oldVal, newVal) -> {rebuildSlotAndTab();});
		model.getRoot().getTypeHierarchy().connectorHeight.addListener(referencer, (prop, oldVal, newVal) -> {rebuildSlotAndTab();});
		
		layoutXProperty().addListener(e -> 
		{
			if(!authorizeLocChange)
				throw new IllegalStateException("Unauthorized layout x change for: "+this+" (non-palette block in a layout pane is illegal)");
			assert layout.x == (int) layoutXProperty().get();
		});
		layoutYProperty().addListener(e -> 
		{
			if(!authorizeLocChange)
				throw new IllegalStateException("Unauthorized layout y change for: "+this+" (non-palette block in a layout pane is illegal)");
			assert layout.y == (int) layoutYProperty().get();
		});
		//Create a reusable listener for the layout of slots
		layoutListener = ((e) -> {if(layout != null) layout.recompute(null, null);});
		//
		if (isListConnectorSlot)
			blocks = null;
		else
		{
			blocks = buildBlocks(model.getChildren());
			model.getChildren().addListener(referencer, (prop, index, oldVal, newVal) ->
			{
				//Cleanup
				removeBlocks(blocks);
				//Rebuild
				blocks = buildBlocks(model.getChildren());
				if(layout != null)
					layout.recompute(null, null);
			});
		}
		layout = new LayoutInfo();
	}

	private void removeBlocks(FXBlock[] blocks)
	{
		for(FXBlock b : blocks)
		{
			//Ensure all listeners are removed since the FXBlock instances are usually reused
			b.layoutListeners.removeActionListener(layoutListener);
			getChildren().remove(b);
		}
	}
	private FXBlock[] buildBlocks(PField<? extends WBlock> pField)
	{
		FXBlock[] r = new FXBlock[pField.size()];
		
		int i = 0;
		for(WBlock b : pField)
		{
			r[i] = (FXBlock) b.getUI();
			getChildren().add(r[i]);
			r[i].layoutListeners.addActionListener(layoutListener);
			i++;
		}
		return r;
	}
	
	private void rebuildSlotAndTab()
	{
		if(slot != null)
		{
			getChildren().remove(slot);
			slot = null;
		}
		if(tab != null)
		{
			getChildren().remove(tab);
			tab = null;
		}
		FXColoredImageView imV;
		
		imV = new FXColoredImageView(model.slotType.get().getContour().getSlotImage(), colorProp.get(), model.bgColor.get());
		imV.colorProperty().bind(colorProp);
		getChildren().add(imV);
		if(!model.isHorizontal())
			imV.rotate(90);
		slot = imV;
		model.bgColor.addListener(slot.referencer(), (prop, oldVal, newVal) -> {if(slot != null) slot.setBGColor(newVal);});
		
		if(model.isMultiSlot && !isListConnectorSlot)
		{
			imV = new FXColoredImageView(model.slotType.get().getContour().getTabImage(), colorProp.get(), null);
			imV.colorProperty().bind(colorProp);
			getChildren().add(imV);
			if(!model.isHorizontal())
				imV.rotate(90);
			tab = imV;
		}
		if(layout != null)
			layout.recompute(null, null);
	}
	public void setSlotLocation(int x, int y)
	{
		authorizeLocChange = true;
		layout.x = x;
		layout.y = y;
		layoutXProperty().set(x);
		layoutYProperty().set(y);
		authorizeLocChange = false;
	}
	public void setSlotX(int x)
	{
		authorizeLocChange = true;
		layout.x = x;
		layoutXProperty().set(x);
		authorizeLocChange = false;
	}
	public void setSlotY(int y)
	{
		authorizeLocChange = true;
		layout.y = y;
		layoutYProperty().set(y);
		authorizeLocChange = false;
	}
	public void setSlotSize(int w, int h)
	{
		if(w < layout.prefW)
			new Throwable("Warning: Assigned width("+w+") is less than preferred width("+layout.prefW+")!").printStackTrace();
		if(h < layout.prefH)
			new Throwable("Warning: Assigned height("+h+") is less than preferred height("+layout.prefH+")!").printStackTrace();
		layout.recompute(w, h);
	}
	/**Returns the x coordinate of the slot. Note that the actual slot image will appear offset by {@value FXBlock#TAB_OFFSET} along the slot's lateral axis.*/
	public int getRealX()
	{
		return layout.x;
	}
	/**Returns the y coordinate of the slot. Note that the actual slot image will appear offset by {@value FXBlock#TAB_OFFSET} along the slot's lateral axis.*/
	public int getRealY()
	{
		return layout.y;
	}
	/**Returns the width of the cell.*/
	public int getRealWidth()
	{
		return layout.getWidth();
	}
	/**Returns the height of the cell.*/
	public int getRealHeight()
	{
		return layout.getHeight();
	}
	/**Returns the width of the slot image, which will be the same size as the tab image.*/
	public int getSlotWidth()
	{
		if(model.isHorizontal())
			return model.getBlock().getRoot().getTypeHierarchy().connectorWidth.get();
		else
			return model.getBlock().getRoot().getTypeHierarchy().connectorHeight.get();
	}
	/**Returns the height of the slot image, which will be the same size as the tab image.*/
	public int getSlotHeight()
	{
		if(model.isHorizontal())
			return model.getBlock().getRoot().getTypeHierarchy().connectorHeight.get();
		else
			return model.getBlock().getRoot().getTypeHierarchy().connectorWidth.get();
	}
	
	@Override
	protected void layoutChildren()
	{
		if(model.isHorizontal())
			slot.relocate(0, FXBlock.TAB_OFFSET);
		else
			slot.relocate(FXBlock.TAB_OFFSET+model.getBlock().getRoot().getTypeHierarchy().connectorHeight.get(), 0);
		if(tab != null)
		{
			if(model.isHorizontal())
				tab.relocate(getRealWidth()+1-getSlotWidth(), FXBlock.TAB_OFFSET);
			else
				tab.relocate(FXBlock.TAB_OFFSET+model.getBlock().getRoot().getTypeHierarchy().connectorHeight.get(), getRealHeight()+1-getSlotHeight());
		}
	}

}
